/*
 * <This Java Class is part of the jMusic API version 1.5, March 2004.>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 *
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */


// GPL code for jMusic CPN.   
// Written by Al Christians (achrist@easystreet.com).
// Copyright  2002, Trillium Resources Corporation, Oregon's
// leading provider of unvarnished software.

package jm.gui.cpn;

import jm.JMC;
import jm.music.data.Note;
import jm.music.data.Part;
import jm.music.data.Phrase;
import jm.music.data.Score;
import jm.util.Write;

import javax.sound.midi.MidiUnavailableException;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;


// Use this class to enter notes for CPN by letter
// Sharps and flats are automatic according to the key
// signature.  

// Add a # to move the note up a half step
// beyond its place according to the key signature.

// Add a - to move the note down a half step
// beyond its place according to the key signature.

// Notes are A thru G for the notes, R for a rest  

// Rhythm value and octave are based on the preceding note.

// Notes are placed to be within 3 notes above or below the
// previous note.  An optional octave number after the note
// will override this default.

// Rhythm is the same as the previous note. Otherwise add
// the following after the note letter:

// W - Whole
// H - Half
// Q - Quarter
// N - Eighth
// X - Sixteenth
// T - Triplet
// & - After a note ties it to the previous note.  These
//     must be the same pitch.  Rhythm values are
//     added.  The next note will almost certainly require
//     an explicit rhythm value, because it will otherwise
//     default to the sum of the two tied notes.  

// Octave number (after the note letter) can be 1 to 9.
// Alternatively put > to move the note up 1 octave fron
// its default position or < to move it down 1 octave

// A period . adds 50% to the rhythm value of the note.
// Put the note letter first, then any of the rhythm or octave
// info that is needed.  Spaces are optional. Use for readability.


public class LetterNotesEditor extends Dialog

        implements ActionListener, WindowListener {

    private static List inputList = new List(8);
    private Button okButton, playButton,
            cancelButton, copyButton;
    private Label inputLabel;
    private TextArea notesInput;
    private Phrase phrase;
    private Stave stave;
    private Note currentNote;
    private char currentNoteLetter = 'A';
    private int currentPitch = 69;

    public LetterNotesEditor(Frame parentFrame) {
        super(
                parentFrame,
                "Set Music Parameters",
                true);
        initializeData();
        initializeButtons();
        initializeLabels();
        setSize(500, 300);
        placeControls();
        addWindowListener(this);
        setVisible(false);
        pack();
    }

    // Get the note letter for a pitch
    private static char getNoteLetter(
            int pitch,
            int keySignature) {

        int notePos = pitch % 12;
        switch (notePos) {
            case 0:
                return 'C';
            case 2:
                return 'D';
            case 4:
                return 'E';
            case 5:
                return 'F';
            case 7:
                return 'G';
            case 9:
                return 'A';
            case 11:
                return 'B';
            default:
                if (keySignature >= 0) {
                    return getNoteLetter(
                            pitch - 1,
                            keySignature
                    );
                } else {
                    return getNoteLetter(
                            pitch + 1,
                            keySignature
                    );
                }
        }
    }

    // See if the pitch is a big jump up
    static boolean pitchIsHigh(
            int pitchValue,
            int currentPitch,
            char letterValue,
            char currentLetter
    ) {
        String notesString = "ABCDEFGABCDEFG";
        if (pitchValue > currentPitch + 8) {
            return true;
        } else if (pitchValue < currentPitch + 3) {
            return false;
        }
        int i, j;
        i = notesString.indexOf(currentLetter);
        j = notesString.indexOf(letterValue, i);
        return j > i + 3;
    }

    // See if the pitch is a big jump down
    static boolean pitchIsLow(
            int pitchValue,
            int currentPitch,
            char letterValue,
            char currentLetter
    ) {
        String notesString = "ABCDEFGABCDEFG";
        if (pitchValue < currentPitch - 8) {
            return true;
        } else if (pitchValue > currentPitch - 3) {
            return false;
        }
        int i, j;
        i = notesString.indexOf(letterValue);
        j = notesString.indexOf(currentLetter, i);
        return j > i + 3;
    }

    private static String crunchLine(String s) {
        StringBuffer b = new StringBuffer(s);
        for (int i = 0; i < b.length(); ++i) {
            if (b.charAt(i) < ' ') {
                b.setCharAt(i, ' ');
            }
        }
        for (int i = 0; i < b.length() - 1; ) {
            if (b.charAt(i) == ' ') {
                if (b.charAt(i + 1) == ' ') {
                    b.deleteCharAt(i);
                } else {
                    i += 2;
                }
            } else {
                ++i;
            }
        }
        return b.toString();
    }

    // Input area has multiple lines.  You can cut and paste in
    // an entire song if you want to.    Save it in a text file
    // with some other editor if you want to.
    private void initializeData() {
        notesInput = new TextArea(
                "", 10, 100,
                TextArea.SCROLLBARS_BOTH);
    }

    private void initializeButtons() {
        okButton = new Button("Add Notes");
        playButton = new Button("Play Notes");
        cancelButton = new Button("Cancel");
        copyButton = new Button("Copy");
    }

    private void initializeLabels() {
        inputLabel = new Label(
                "Enter Note Names, R for Rest"
        );
    }

    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == okButton) {
            addNotes();
            dispose();
        } else if (e.getSource() == playButton) {
            playNotes();
        } else if (e.getSource() == cancelButton) {
            dispose();
        } else if (e.getSource() == copyButton) {
            if (inputList.getSelectedItem().length() > 0) {
                notesInput.setText(inputList.getSelectedItem());
            }
        }
    }

    // Add the notes from the input edit control to the phrase.
    private void addNotes() {
        initializeUpdate();
        String s = notesInput.getText();
        for (int i = 0; i < s.length(); ++i) {
            processChar(Character.toUpperCase(s.charAt(i)));
        }
        inputList.add(crunchLine(s), 0);
        while (inputList.getItemCount() > 100) {
            inputList.remove(100);
        }
    }

    // Play the notes in the input edit control to hear if
    // you want to add them to the phrase
    private void playNotes() {
        Phrase savePhrase = phrase.copy();
        initializeUpdate();
        String s = notesInput.getText();
        for (int i = 0; i < s.length(); ++i) {
            processChar(Character.toUpperCase(s.charAt(i)));
        }
        for (int i = 0; i < savePhrase.size(); ++i) {
            phrase.removeNote(0);
        }
        try {
            JmMidiPlayer midiPlayer = new JmMidiPlayer();
            Score sc = new Score();
            Part p = new Part();
            sc.addPart(p);
            p.addPhrase(phrase);
            Write.midi(sc, midiPlayer);
            midiPlayer.play();
            midiPlayer.close();
        } catch (MidiUnavailableException ex) {
            System.out.println(
                    "Midi Not Available"
            );
        }
        phrase.empty();
        for (int i = 0; i < savePhrase.size(); ++i) {
            phrase.addNote(savePhrase.getNote(i));
        }
    }

    // Set up the initial default rhythm and octave values
    private void initializeUpdate() {
        if (phrase.size() > 0) {
            currentNote = phrase.getNote(phrase.size() - 1);
            if (currentNote.getPitch() >= 0) {
                currentPitch = currentNote.getPitch();
                currentNoteLetter
                        = getNoteLetter(
                        currentPitch,
                        stave.getKeySignature());
            }
        } else {
            currentNote = null;
        }
    }

    // Add the note for a letter to the phrase
    private void processChar(char c) {
        switch (c) {
            case 'A':
                addNote(JMC.a4, c);
                break;
            case 'B':
                addNote(JMC.b4, c);
                break;
            case 'C':
                addNote(JMC.c4, c);
                break;
            case 'D':
                addNote(JMC.d4, c);
                break;
            case 'E':
                addNote(JMC.e4, c);
                break;
            case 'F':
                addNote(JMC.f4, c);
                break;
            case 'G':
                addNote(JMC.g4, c);
                break;
            case 'R':
                addNote(JMC.REST, c);
                break;
            case '#':
            case '+':
                sharpenNote();
                break;
            case '-':
                flattenNote();
                break;
            case '>':
                addOctave();
                break;
            case '<':
                subtractOctave();
                break;

            case '1':
                moveToInterval(JMC.cf1, JMC.bs1);
                break;
            case '2':
                moveToInterval(JMC.cf2, JMC.bs2);
                break;
            case '3':
                moveToInterval(JMC.cf3, JMC.bs3);
                break;
            case '4':
                moveToInterval(JMC.cf4, JMC.bs4);
                break;
            case '5':
                moveToInterval(JMC.cf5, JMC.bs5);
                break;
            case '6':
                moveToInterval(JMC.cf6, JMC.bs6);
                break;
            case '7':
                moveToInterval(JMC.cf7, JMC.bs7);
                break;
            case '8':
                moveToInterval(JMC.cf8, JMC.bs8);
                break;
            case '9':
                moveToInterval(JMC.cf9, JMC.g9);
                break;

            case '.':
                dotNote();
                break;
            case 'H':
                makeHalfNote();
                break;
            case 'W':
                makeWholeNote();
                break;
            case 'Q':
                makeQuarterNote();
                break;
            case 'N':
                makeEighthNote();
                break;
            case 'T':
                makeTriplet();
                break;
            case 'X':
                makeSixteenthNote();
                break;
            case '&':
                tieNotes();
                break;

            default: {
            }
            break;
        }
    }

    // Add a note to the current phrase
    private void addNote(int pitchValue, char charValue) {
        Note newNote;
        int pv = pitchValue;
        if (currentNote != null) {
            newNote = currentNote.copy();
        } else {
            newNote = new Note();
        }
        if (charValue != 'R') {
            if (pv > currentPitch) {
                while (pitchIsHigh(
                        pv,
                        currentPitch,
                        charValue,
                        currentNoteLetter)) {
                    pv -= 12;
                }
            } else {
                while (pitchIsLow(
                        pv,
                        currentPitch,
                        charValue,
                        currentNoteLetter)) {
                    pv += 12;
                }
            }
        }
        if (noteIsSharp(charValue)) {
            ++pv;
        }
        if (noteIsFlat(charValue)) {
            --pv;
        }
        newNote.setPitch(pv);
        phrase.add(newNote);
        currentNote = newNote;
        currentNoteLetter = charValue;
        if (currentNoteLetter != 'R') {
            currentPitch = pv;
        }
    }

    private void sharpenNote() {
        currentNote.setPitch(currentNote.getPitch() + 1);
        currentPitch = currentNote.getPitch();
    }

    private void flattenNote() {
        currentNote.setPitch(currentNote.getPitch() - 1);
        currentPitch = currentNote.getPitch();
    }

    private void addOctave() {
        currentNote.setPitch(currentNote.getPitch() + 12);
        currentPitch = currentNote.getPitch();
    }

    private void subtractOctave() {
        currentNote.setPitch(currentNote.getPitch() - 12);
        currentPitch = currentNote.getPitch();
    }

    private void moveToInterval(int from, int thru) {
        while (currentNote.getPitch() > thru) {
            currentNote.setPitch(currentNote.getPitch() - 12);
        }
        while (currentNote.getPitch() < from) {
            currentNote.setPitch(currentNote.getPitch() + 12);
        }
        if (currentNote.getPitch() == from) {
            if (currentNoteLetter == 'B') {
                currentNote.setPitch(currentNote.getPitch() + 12);
            }
        }
        if (currentNote.getPitch() == thru) {
            if (currentNoteLetter == 'C') {
                currentNote.setPitch(currentNote.getPitch() - 12);
            }
        }
        currentPitch = currentNote.getPitch();
    }

    private void dotNote() {
        adjustNoteByFactor(1.5);
    }

    private void makeHalfNote() {
        adjustNoteByFactor(
                JMC.HALF_NOTE / currentNote.getRhythmValue());
    }

    private void makeWholeNote() {
        adjustNoteByFactor(
                JMC.WHOLE_NOTE / currentNote.getRhythmValue());
    }

    private void makeQuarterNote() {
        adjustNoteByFactor(
                JMC.QUARTER_NOTE / currentNote.getRhythmValue());
    }

    private void makeEighthNote() {
        adjustNoteByFactor(
                JMC.EIGHTH_NOTE / currentNote.getRhythmValue());
    }

    private void makeTriplet() {
        adjustNoteByFactor(
                JMC.QUAVER_TRIPLET
                        / currentNote.getRhythmValue()
        );
    }

    private void makeSixteenthNote() {
        adjustNoteByFactor(
                JMC.SIXTEENTH_NOTE
                        / currentNote.getRhythmValue()
        );
    }

    private void tieNotes() {
        if (phrase.size() > 1) {
            phrase.removeLastNote();
            Note lastNote;
            lastNote = phrase.getNote(phrase.size() - 1);
            lastNote.setDuration(
                    lastNote.getDuration()
                            + currentNote.getDuration()
            );
            lastNote.setRhythmValue(
                    lastNote.getRhythmValue()
                            + currentNote.getRhythmValue()
            );
            currentNote = lastNote;
        }
    }

    private boolean noteIsSharp(char charValue) {
        boolean answer;
        String sharpNotes;
        sharpNotes = "FCGDAEB";
        int numSharps = stave.getKeySignature();
        if (charValue == 'R') {
            answer = false;
        } else if (numSharps > 0) {
            answer = sharpNotes.indexOf(charValue)
                    < numSharps;
        } else {
            answer = false;
        }
        return answer;
    }

    private boolean noteIsFlat(char charValue) {
        boolean answer;
        String flatNotes;
        flatNotes = "BEADGCF";
        int numFlats = -stave.getKeySignature();
        if (charValue == 'R') {
            answer = false;
        } else if (numFlats > 0) {
            answer = flatNotes.indexOf(charValue)
                    < numFlats;
        } else {
            answer = false;
        }
        return answer;
    }

    private void adjustNoteByFactor(double x) {
        currentNote.setRhythmValue(
                x * currentNote.getRhythmValue());
        currentNote.setDuration(
                x * currentNote.getDuration());
    }

    private void placeControls() {
        GridBagLayout layout = new GridBagLayout();
        GridBagConstraints c = new GridBagConstraints();
        setLayout(layout);

        c.fill = GridBagConstraints.BOTH;
        c.weightx = 0.5;
        c.gridwidth = 5;
        c.gridheight = 1;

        c.gridx = 0;
        c.gridy = 0;
        layout.setConstraints(inputLabel, c);
        add(inputLabel);

        c.gridx = 0;
        c.gridy = 1;
        c.gridheight = 10;
        layout.setConstraints(notesInput, c);
        add(notesInput);

        c.gridx = 2;
        c.gridy = 12;
        c.gridheight = 1;
        c.gridwidth = 1;
        layout.setConstraints(okButton, c);
        add(okButton);
        okButton.addActionListener(this);

        c.gridx = 3;
        c.gridy = 12;
        c.gridwidth = 1;
        layout.setConstraints(playButton, c);
        add(playButton);
        playButton.addActionListener(this);

        c.gridx = 4;
        c.gridy = 12;
        c.gridwidth = 1;
        layout.setConstraints(cancelButton, c);
        add(cancelButton);
        cancelButton.addActionListener(this);

        c.gridx = 1;
        c.gridy = 15;
        c.gridwidth = 5;
        c.gridheight = 5;
        layout.setConstraints(inputList, c);
        add(inputList);

        c.gridx = 1;
        c.gridy = 20;
        c.gridwidth = 5;
        c.gridheight = 1;
        layout.setConstraints(copyButton, c);
        add(copyButton);
        copyButton.addActionListener(this);

    }

    // Show the screen and process input
    public void getNotes(Stave theStave) {
        phrase = theStave.getPhrase();
        stave = theStave;
        setLocation(200, 50);
        show();
    }

    public void windowOpened(WindowEvent e) {
    }

    public void windowClosing(WindowEvent e) {
        if (e.getSource() == this) dispose();
    }


    public void windowClosed(WindowEvent e) {
    }

    /**
     * Invoked when a window is iconified.
     */
    public void windowIconified(WindowEvent e) {
    }

    /**
     * Invoked when a window is de-iconified.
     */
    public void windowDeiconified(WindowEvent e) {
    }

    /**
     * Invoked when a window is activated.
     */
    public void windowActivated(WindowEvent e) {
    }

    /**
     * Invoked when a window is de-activated.
     */
    public void windowDeactivated(WindowEvent e) {
    }


}
